﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace ExpressionMapper
{
    using MappingExpressionGenerator = Func<Expression, Type, Expression, ParameterExpression, Expression>;

    /// <summary>
    /// Static facade for Mapper.
    /// </summary>
    public static class Mapper
    {
        private class ConversionInfo
        {
            private readonly Type _fromType;
            private readonly Type _toType;

            internal ConversionInfo(Type fromType, Type toType)
            {
                _fromType = fromType;
                _toType = toType;
            }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof(ConversionInfo)) return false;
                return Equals((ConversionInfo)obj);
            }

            public bool Equals(ConversionInfo other)
            {
                if (ReferenceEquals(null, other)) return false;
                if (ReferenceEquals(this, other)) return true;
                return other._fromType.FullName == _fromType.FullName && other._toType.FullName == _toType.FullName;
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return ((_fromType != null ? _fromType.GetHashCode() : 0) * 397) ^ (_toType != null ? _toType.GetHashCode() : 0);
                }
            }
        }

        private static readonly IDictionary<ConversionInfo, Delegate> _mappers = new Dictionary<ConversionInfo, Delegate>();

        private static readonly IEnumerable<MappingExpressionGenerator> _generators;
        private static readonly MappingExpressionGenerator _aggregate;
        private static readonly Dictionary<MappedMembers, IMemberExtractor> _memberExtractors;

        static Mapper()
        {
            _memberExtractors =
                new Dictionary<MappedMembers, IMemberExtractor>
                    {
                        {MappedMembers.AllFields, new AllFieldsExtractor()},
                        {MappedMembers.PublicMembers, new PublicMembersExtractor()},
                        {MappedMembers.PublicMembersWithFlattering, new PublicMembersWithFlatteringExtractor()},
                    };

            _aggregate =
                (fromExpr, to, customMapping, instanceDictionary) =>
                _generators.Select(f => f(fromExpr, to, customMapping, instanceDictionary)).FirstOrDefault(e => e != null);

            _generators = new[]
                              {
                                  Generators.ToNullable(),
                                  Generators.FromNullable(),
                                  Generators.Assignable(),
                                  Generators.UseConvertClass(),
                                  Generators.UseIConvertible(),
                                  Generators.UseObjectCasting(),
                                  Generators.ArrayToArray(),
                                  Generators.ListToList(),
                                  Generators.DictionaryToDictionary(),
                                  Generators.StackToStack(),
                                  Generators.ConcurrentCollectionToConcurrentCollection(),
                                  Generators.SpecializedCollectionToSpecializedCollection(),
                                  Generators.StandartCollectionToStandartCollection(),
                                  //Generators.CollectionToCollection(_aggregate),
                                  Generators.ComplexToComplex(_aggregate,
                                                              _memberExtractors[MappedMembers.PublicMembersWithFlattering]),
                                  Generators.UseToString(),
                                  Generators.Serializable(_aggregate),
                              };
        }

        /// <summary>
        /// Returns a Mapper instance for specified types.
        /// </summary>
        /// <typeparam name="T">Type of source object</typeparam>
        /// <returns></returns>
        public static Func<T, T> Create<T>()
        {
            return Create<T, T>(null);
        }


        /// <summary>
        /// Returns a Mapper instance for specified types.
        /// </summary>
        /// <typeparam name="TFrom">Type of source object</typeparam>
        /// <typeparam name="TTo">Type of destination object</typeparam>
        /// <returns></returns>
        public static Func<TFrom, TTo> Create<TFrom, TTo>()
        {
            return Create<TFrom, TTo>(null);
        }

        /// <summary>
        /// Returns a Mapper instance for specified types.
        /// </summary>
        /// <typeparam name="TFrom">Type of source object.</typeparam>
        /// <typeparam name="TTo">Type of destination object.</typeparam>
        /// <param name="customMapping">Lambda for custom mapping.</param>
        /// <returns></returns>
        public static Func<TFrom, TTo> Create<TFrom, TTo>(Expression<Func<TFrom, TTo>> customMapping)
        {
            var dictionaryMapper = GetMapper(typeof(TFrom), typeof(TTo), customMapping);
            Func<TFrom, TTo> resultMapper = from => dictionaryMapper(from, new ReferenceDictionary());
            return resultMapper;
        }

        /// <summary>
        /// Returns a Mapper instance for specified types.
        /// </summary>
        /// <typeparam name="TFrom">Type of source object</typeparam>
        /// <typeparam name="TTo">Type of destination object</typeparam>
        /// <returns></returns>
        public static Func<object, object> Create(Type type)
        {
            return Create(type, type);
        }

        /// <summary>
        /// Returns a Mapper instance for specified types.
        /// </summary>
        /// <typeparam name="TFrom">Type of source object</typeparam>
        /// <typeparam name="TTo">Type of destination object</typeparam>
        /// <returns></returns>
        public static Func<object, object> Create(Type fromType, Type toType)
        {
            var dictionaryMapper = GetMapper<object, object>(fromType, toType, null);
            Func<object, object> resultMapper = from => dictionaryMapper(from, new ReferenceDictionary());
            return resultMapper;
        }

        internal static Func<TFrom, ReferenceDictionary, TTo> GetMapper<TFrom, TTo>(Type fromType, Type toType,
            Expression<Func<TFrom, TTo>> customMapping)
        {
            Delegate mapper;
            ConversionInfo info = new ConversionInfo(fromType, toType);

            if (!_mappers.TryGetValue(info, out mapper))
            {
                mapper = _aggregate.GetMapper(fromType, toType, customMapping);
                _mappers.Add(info, mapper);
            }

            Func<TFrom, ReferenceDictionary, TTo> result = (from, dictionary) =>
                                                                      {
                                                                          try
                                                                          {
                                                                              return (TTo)mapper.DynamicInvoke(@from, dictionary);
                                                                          }
                                                                          catch (Exception excp)
                                                                          {
                                                                              if (excp.InnerException != null)
                                                                                  throw excp.InnerException;

                                                                              throw;
                                                                          }
                                                                      };

            return result;
        }

        internal static TTo Map<TFrom, TTo>(TFrom obj, ReferenceDictionary referenceDictionary)
        {
            Type fromType = typeof(TFrom);
            Type toType = typeof(TTo);

            TFrom nullObj = default(TFrom);
            if (Equals(nullObj, obj) && (fromType.IsClass || Generators.IsNullable(fromType)))
                return default(TTo);

            Type objectType = obj.GetType();

            if (Generators.IsObject(fromType))
            {
                if (Generators.IsObject(toType))
                    return (TTo)GetMapper<object, object>(objectType, objectType, null)(obj, referenceDictionary);

                return GetMapper<object, TTo>(objectType, toType, null)(obj, referenceDictionary);
            }
            if (Generators.IsObject(toType))
            {
                if (!objectType.IsAssignableFrom(fromType))
                    return GetMapper<TFrom, TTo>(fromType, fromType, null)(obj, referenceDictionary);

                return GetMapper<TFrom, TTo>(fromType, objectType, null)(obj, referenceDictionary);
            }
            if (toType.IsAssignableFrom(fromType))
            {
                return GetMapper<TFrom, TTo>(objectType,objectType, null)(obj, referenceDictionary);
            }
            return GetMapper<TFrom, TTo>(fromType, toType, null)(obj, referenceDictionary);
        }

        #region Nested type: MappedMembers

        private enum MappedMembers
        {
            AllFields,
            PublicMembers,
            PublicMembersWithFlattering
        }

        #endregion
    }
}